Spring Security 上
Security-dome
1.创建项目
创建一个Spring Boot项目,不用加入什么依赖
2.导入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
|
3.创建控制层
@RestController public class TestController { @GetMapping("/hello") public String hello(){ return "hello Security"; } }
|
4.配置文件修改端口号
5.运行测试
运行网址为:
http://localhost:8081/hello
这时候会发现,网址会自动变为:
http://localhost:8081/login
6.登录
能看到,在该页面中有账号
和密码
默认账号:user
默认密码:
登录之后:
Security 原理
Spring Security 本质是一个过滤器链
FilterSecurityInterceptor:是一个方法级的 权限过滤器
,基本位于过滤链的最底部
ExceptionTranslationFilter:是个异常过滤器
,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter:对 /login
的POST请求做拦截,校验表单中用户名,密码
过滤器加载步骤
步骤流程
使用Spring Security配置过滤器 : DelegatingFilterProxy
源代码如下
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized(this.delegateMonitor) { delegateToUse = this.delegate; if (delegateToUse == null) { WebApplicationContext wac = this.findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?"); }
delegateToUse = this.initDelegate(wac);
} this.delegate = delegateToUse; } } this.invokeDelegate(delegateToUse, request, response, filterChain); }
|
即为:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { delegateToUse = this.initDelegate(wac); }
|
然后我们查看 initDelegate:
初始化为 FilterChainProxy 对象
进入 FilterChainProxy:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (!clearContext) { this.doFilterInternal(request, response, chain); } else { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); this.doFilterInternal(request, response, chain); } catch (RequestRejectedException var9) { this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } }
|
可以看出,无论满不满足条件,最终都需要运行 doFilterInternal()方法
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
|
private List<Filter> getFilters(HttpServletRequest request) { int count = 0; Iterator var3 = this.filterChains.iterator(); SecurityFilterChain chain; do { if (!var3.hasNext()) { return null; } chain = (SecurityFilterChain)var3.next(); if (logger.isTraceEnabled()) { ++count; logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size())); } } while(!chain.matches(request)); return chain.getFilters(); }
|
所以 doFilterInternal() 方法 可以返回 所有要进行加载的过滤器
总结:
- 配置过滤器 DelegatingFilterProxy
- 在其中进行初始化 initDelegate
- 在初始化中得到 FilterChainProxy 对象
- 在其中运行的就是 doFilterInternal() 方法,该方法返回的就是 所有要进行加载的过滤器
UserDetailsService 接口
UserDetailsService接口 : 查询数据库用户名和密码过程
步骤:
- 创建类继承UsernamePasswordAuthenticationFilter,重写三个方法: attemptAuthentication() 、successfulAuthentication()、unsuccessfulAuthentication()
- 如果成功调用successfulAuthentication(),反之调用unsuccessfulAuthentication()
- 创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是安全框架提供对象
PasswordEncoder接口
PasswordEncoder接口 : 数据加密接口,用于返回User对象里面密码加密
加密方法:
BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.
BCryptPasswordEncoder b = new BCryptPasswordEncoder(); String zc = b.encode("zc");
|
Web权限
在 Security-dome
中可以看到,如果想要进入页面,还需要输入账号密码
而对于登陆时候的账号密码可以进行自定义设置:
- 通过配置文件
- 通过配置类
- 自定义编写实现类
1.通过配置文件
spring.security.user.name=root spring.security.user.password=root
|
这个时候再运行,会发现控制台不会出现密码
,可以直接通过设置的账号密码登录
2.通过配置类
- 创建一个 SecurityConfig 配置类
- 重写configure()方法,注意看清参数,不要选错方法
- 很重要的一点:需要注入PasswordEncoder接口
如果不注入该接口,可能报 Encoded password does not look like BCrypt
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String password = bCryptPasswordEncoder.encode("root"); auth.inMemoryAuthentication() .withUser("root") .password(password) .roles("admin"); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
|
这时候,也可以直接使用你设置的账号密码登录页面
3.自定义编写实现类
- 编写userDetailsService实现类,返回User对象
- 创建一个 SecurityConfig 配置类
编写一个UserDetailsService实现类
在其中需要重写 loadUserByUsername() 方法,该方法用于登录
@Service("userDetailsService") public class MyuserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("root", new BCryptPasswordEncoder().encode("root"),auths); } }
|
UserDetailsService 解析
对于该实现类中重写的 loadUserByUsername() 方法,返回的是 UserDetails 接口
在源代码中可以看出,实际上 UserDetails 接口,返回的是一个 User 对象
而在User对象中,需要返回三个参数:
String、String、Collection;
账号 、 密码 、集合(权限等信息)
创建一个 SecurityConfig 配置类
@Configuration public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired UserDetailsService userDetailsService;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
@Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
|
这时候测试,也可以直接使用设置的账号密码登录
之后,如果连接数据库,一般都是用第三种方式
4.连接数据库完成用户认证
(该方法是在第三种方法代码基础上完成)
- 创建数据库
- 整合Mybatis-Plus完成数据库操作
- 配置JDBC信息
- 创建实体类、Mapper接口
- 创建UserDetailsService类
创建数据库
创建了一个 mybatis-plus
数据库 ,其中创建了一个users
表,记得创建后,加入数据
引入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
|
配置JDBC信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root
|
创建实体类、Mapper接口
@Data public class Users { private Integer id; private String username; private String password; }
|
@Repository @Mapper public interface UsersMapper extends BaseMapper<Users> { }
|
创建UserDetailsService类
@Service("userDetailsService") public class MyuserDetailsService implements UserDetailsService { @Autowired private UsersMapper usersMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper<>(); wrapper.eq("username",username); Users users = usersMapper.selectOne(wrapper); if (users == null){ throw new UsernameNotFoundException("用户名不存在"); } List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths); } }
|
这时候就可以正常运行了
5.自定义登录页面
在上面代码的基础上完成该部分代码
1.创建前端页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post" action="/user/login"> 用户名:<input type="text" name="username"> <br> 密码:<input type="text" name="password"> <br> <input type="submit" value="login"> </form> </body> </html>
|
2.书写Controller层代码
@GetMapping("/index") public String index(){ return "index"; }
|
3.在创建的配置类中重写 configure(HttpSecurity http) 方法
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/login") .defaultSuccessUrl("/index").permitAll() .and().authorizeRequests() .antMatchers("/","/hello","/login") .permitAll() .anyRequest().authenticated() .and().csrf().disable(); }
|
这时候可以分别测试进入以下两个路径:
http://localhost:8081/hello
http://localhost:8081/index
会发现,第一个 hello 路径 ,不会拦截了,可以直接进入页面
第二个index,会进入自定义的登陆页面,登陆成功后,才可以进入
基于角色或权限的访问控制
1.hasAuthority方法
如果当前的主体具有指定的权限,则返回true,否则返回false
- 修改配置类
- 在 UserDetailsService 实现类中添加权限
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .defaultSuccessUrl("/index").permitAll() .and().authorizeRequests() .antMatchers("/","/hello","/user/login") .permitAll() .antMatchers("/index").hasAuthority("admins") .anyRequest().authenticated() .and().csrf().disable(); }
|
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper<>(); wrapper.eq("username",username); Users users = usersMapper.selectOne(wrapper); if (users == null){ throw new UsernameNotFoundException("用户名不存在"); } List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths); }
|
进行测试,路径为:
http://localhost:8081/index
2.hasAnyAuthority方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true
与 hasAuthority() 的区别是
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .defaultSuccessUrl("/index").permitAll() .and().authorizeRequests() .antMatchers("/","/hello","/user/login") .permitAll()
.antMatchers("/index").hasAnyAuthority("admins","user")
.anyRequest().authenticated() .and().csrf().disable(); }
|
3.hasRole方法
如果用户具备给定角色就 允许访问,否则出现 403
如果当前主体具有指定的角色,则返回 true
该方法与 hasAuthority 方法,使用方法基本相同,区别就是 他需要在权限前加上 ROLE_
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .defaultSuccessUrl("/index").permitAll() .and().authorizeRequests() .antMatchers("/","/hello","/user/login") .permitAll()
.antMatchers("/index").hasRole("user")
.anyRequest().authenticated() .and().csrf().disable(); }
|
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_user");
|
这时候可以正常运行
4.hasAnyRole方法
表示用户具备任何一个条件都可以访问
该方法与 hasRole() 的区别 与1 2 两种方法相同,大家可以自行测试
5.自定义403页面
1.创建自定义403页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>没有权限访问!!!</h1> </body> </html>
|
2.修改配置类
@Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedPage("/uuauth.html"); }
|